/*
 *  ValueRef.java
 *
 *  $Id: ValueRef.java 37 2012-04-16 13:33:35Z johann.petrak@gmail.com $
 */

package at.ofai.gate.japeutils.ops;

import gate.Annotation;
import gate.AnnotationSet;
import gate.FeatureMap;
import gate.util.GateRuntimeException;
import gate.jape.constraint.*;
import gate.jape.*;
import java.util.Map;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;


/**
 * A ConstraintPredicate that makes it possible to use value back-references
 * in JAPE rules.
 * <p>
 * The ConstraintPredicate "valueref" must be used with one of the following
 * operators:
 * <ul>
 * <li>!=~ This operator should be used once in its own phase to reset 
 * the data structures between documents
 * <li>!~ This operator should be used as the first operator in a  sequence
 * where identical values should be matched and will initialize a reference
 * with the value.
 * <li>== This operator should be used in all subsequent locations where the
 * value that has been assigned to a reference already should be matched.
 * </ul>
 * <p>
 * Here is an example for a phase to initialize the datastructures. Note that
 * the reference does not matter and only one reference name needs to be used,
 * even if the rest of the phases uses several different reference names.
 * <pre>
 * Phase: reset
 * Input: Token
 * Options: control = once
 * Rule: reset1
 * ({Token valueref {Token.string !=~ "ref"}}) --> {}
 * </pre>
 * <p>
 * <p>
 * Here is an example for a rule that makes shure that three tokens with the
 * same values for feature pos occur after one another:
 * <pre>
 * ({Token valueref {Token.pos !~ "ref"}} 
 *  {Token valueref {Token.pos == "ref"}}
 *  {Token valueref {Token.pos == "ref"}}):label
 * -->:label.ThreeTimes = {}
 * </pre>
 * @author Johann Petrak
 */
public class ValueRef implements ConstraintPredicate {
  
  private static final long serialVersionUID = -2369138804874616879L;
  protected AnnotationAccessor accessor;
  protected Object value;
  
  static Map<String, Set<Object>> valueRefsCur = 
    new HashMap<String,Set<Object>>();
  static Map<String, Set<Object>> valueRefsLast = 
    new HashMap<String,Set<Object>>();
  static Map<String, Integer> curOffsets = 
    new HashMap<String, Integer>();
  
  protected String referenceName = "";
  protected String featureName = "";
  private static final int OPERATOR_EQUALS = 1;
  private static final int OPERATOR_NOTEQUALS = 2;
  private static final int OPERATOR_RESET = 0;
  private static final int OPERATOR_GREATER=3;
  private static final int OPERATOR_GREATEREQUALS=4;
  private static final int OPERATOR_LESS=5;
  private static final int OPERATOR_LESSEQUALS=6;
  private static final int OPERATOR_CLEAR=7;
  
  protected int operator = OPERATOR_EQUALS;
  
 
  public ValueRef() {
    //System.out.println("MyConstraint: default constructor ");    
  }
  public ValueRef(AnnotationAccessor accessor, Object value) {
    // TODO: when is this ever called?
    //System.out.println("MyConstraint: two args constructor "+accessor+"/"+value);
  }

  public boolean matches(Annotation annot, AnnotationSet context) 
    throws GateRuntimeException
  {
    // The clear operator is used in a phase to make sure everything is
    // reset between document and phases
    if(operator == OPERATOR_CLEAR) {
      curOffsets.clear();
      valueRefsCur.clear();
      valueRefsLast.clear();
      //System.out.println("Clearing for="+referenceName);
      return true;
    }
    
    Object featureValue = getFeature(annot.getFeatures(),featureName);
    int offset = annot.getStartNode().getOffset().intValue();
    Integer curOffset = curOffsets.get(referenceName);
    if(curOffset == null) {
      curOffset = -1;
    }
    //System.out.println("curoffset="+curOffset+" offset="+offset);
    
    // if we get a reset operator, what we do depends on the offset:
    // if it is the same offset as last time, we obviously have another 
    // annotation of the target type at the same offset and we add its value
    // to the current set of values. If the offset is different, we need to
    // start a new cycle and we delete the old sets for this referenceName
    // and create a new current set for it.
    if(operator == OPERATOR_RESET) {
      if(curOffset == offset) {
        //System.out.println("Reset/add: referenceName="+referenceName+", feature="+featureName+", offset="+offset+", val="+featureValue);
        valueRefsCur.get(referenceName).add(featureValue);
      } else {
        //System.out.println("Reset/new: referenceName="+referenceName+", feature="+featureName+", offset="+offset+", val="+featureValue);
        curOffsets.put(referenceName,offset);
        Set<Object> newset = new HashSet<Object>();
        newset.add(featureValue);
        valueRefsCur.put(referenceName, newset);
        valueRefsLast.remove(referenceName);
      }
      return true;
    } 
    
    // Everything else is a comparison with values set earlier. If we make 
    // a comparison at a location where we made a comparison earlier, 
    // just check if the current value matches the last valueRefs and add it
    // to the current set if yes and return true.
    // If we have a new offset, the current valueRefs become the lastRefs and
    // a new current valueRefs set is created.
    if(curOffset != offset) {
      curOffsets.put(referenceName,offset);
      //System.out.println("Compare/new: referenceName="+referenceName+", feature="+featureName+", offset="+offset+", val="+featureValue);
      valueRefsLast.put(referenceName, valueRefsCur.get(referenceName));
      valueRefsCur.put(referenceName, new HashSet<Object>());
    }
    // Comparisons are all made by comparing the current value to all values
    // in the last set. If a match is found the value is added to the curset
    // and true is returned, otherwise we just return false.
    if(operator == OPERATOR_EQUALS) {      
      boolean ret = equal(valueRefsLast.get(referenceName),featureValue);
      //System.out.println("Compare/equal: referenceName="+referenceName+", feature="+featureName+", offset="+offset+", val="+featureValue+" ret="+ret);
      if(ret) {
        valueRefsCur.get(referenceName).add(featureValue);
      }
      return ret;
    } else {
      // this should never occur as we check and set the operator in setValue
      throw new GateRuntimeException("Operator not (yet) supported");
    }
  }
  public void setAccessor(AnnotationAccessor accessor) {
    this.accessor = accessor;
    //System.out.println("MyConstraint: setAccessor "+accessor);
  }
  public AnnotationAccessor getAccessor() {
    //System.out.println("MyConstraint: getAccessor");
    return this.accessor;
  }
  public void setValue(Object value) {
    this.value = value;
    //System.out.println("setValue for "+value);
    if(!(value instanceof Constraint)) {
      throw new GateRuntimeException("Not a constraint: "+value);
    }
    Constraint constraint = (Constraint)value;
    if(constraint.isNegated()) {
      throw new GateRuntimeException("Constraint must not be negated: "+constraint);
    }
    if(constraint.getAttributeSeq().size() != 1) {
      throw new GateRuntimeException("Constraint must have one predicate: "+constraint);
    }
    String annType = constraint.getAnnotType();
    //System.out.println("Annotation type is "+annType);
    ConstraintPredicate pred = constraint.getAttributeSeq().get(0);
    String op = pred.getOperator();
    //System.out.println("Operator is "+op);
    if(op.equals("!~")) {
      operator = OPERATOR_RESET;
    } else if(op.equals("!=~")) {
      operator = OPERATOR_CLEAR;
    } else if(op.equals("==")) {
      operator = OPERATOR_EQUALS;
    } else {
      //System.err.println("Operator is "+op+" but must be one of !=~ (reset), ==, or !=");
      throw new GateRuntimeException("Constraint operator must be !~ (reset),  !=~ (clear), or == (equal to ref)");
    }
    //System.out.println("setValue op is "+op);
    Object predObj = pred.getValue();
    //if(!(predObj instanceof String)) {
    //  throw new GateRuntimeException("Value of predicate must be a string: "+predObj+" not a "+predObj.getClass());
    //}
    // String predValue = (String)predObj;
    String predValue = predObj.toString();
    //System.out.println("setValue pred value is "+predValue);
    
    //System.out.println("Value is "+predValue);
    referenceName = predValue;
    AnnotationAccessor aa = pred.getAccessor();
    String key = aa.getKey().toString();
    //System.out.println("setValue key is "+key);
    featureName = key;
    //System.out.println("Initialized constraint: ref="+referenceName+", feature="+featureName);
  }
  public Object getValue() {
    //System.out.println("MyConstraint: getValue: "+this.value);
    return this.value;
  }
  public String getOperator() {
    //System.out.println("MyConstraing: getOperator: "+"valueref");
    return "valueref";
  }
  
  // returns true if the object b is equal to one of the objects in the set
  protected boolean equal(Set<Object> set, Object b) {
    if(b == null) { return false; }
    for(Object a : set) {
      if (a.equals(b)) { return true; } 
    } 
    return false;
  }
  
  // returns a.compareTo(b) if both are of the same class and Comparable
  // otherwise throws an exception.
  protected int compare(Object a, Object b) {
    if(a != null && b != null) {
      if(a.getClass().equals(b.getClass())) {
        if(a instanceof Comparable) {
          return ((Comparable)a).compareTo((Comparable)b);
        } else {
          throw new GateRuntimeException(
            "less than or equal comparison but not both are comparable: a="
            +a+",b="+b);
        }
      } else {
          throw new GateRuntimeException(
            "less than or equal comparison but not both of the same class: a="
            +a+",b="+b);
      }
    } else {
          throw new GateRuntimeException(
            "less than or equal comparison but not both different from null: a="
            +a+",b="+b);
    }
  }
  
  private Object getFeature(FeatureMap fm, String name) {
    Object ret = fm.get(name);
    if(ret == null) {
      warnOnce("Null value in valueRef for feature "+name);
    }
    return ret;
  }
  
  private void warnOnce(String message) {
    if(!alreadyWarned.contains(message)) {
      alreadyWarned.add(message);
      System.out.println(message);
    }
  }
  
  private Set<String> alreadyWarned = new HashSet<String>();
  
}
